/*
 * Copyright 2014 JootMir Project
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * Support: http://www.jootmir.org
 */
package org.jootmir.client.core;

import static org.lwjgl.opengl.GL11.*;

import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import javax.swing.JFrame;

import org.apache.log4j.Logger;
import org.jootmir.client.core.exception.JMir2Exception;
import org.jootmir.client.core.util.Kit;
import org.lwjgl.LWJGLException;
import org.lwjgl.Sys;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GLContext;

/**
 * 游戏主程
 * <br>
 * 使用{@link #initialize(ApplicationConfig, ApplicationListener) initialize}来初始化一个游戏主程
 * <br>
 * 并使用{@link #mainLoop() mainLoop}开始游戏主循环，此时线程会产生阻塞，直至游戏结束
 * <br>
 * 在{@link ApplicationListener applicationListener}中实现您的业务逻辑
 * 
 * @author ShawRyan
 */
public final class Application {
	/** log */
	private static Logger log = Logger.getLogger(Application.class);
	/** 图形信息 */
	private Graphics graphics = Graphics.get();
	/** 游戏 */
	private ApplicationListener listener;
	/** 游戏进行时 */
	private volatile boolean running = true;
	/** 缓存 */
	private AllCache cache = AllCache.get();
	/** 父窗口(以窗口状态运行时) */
	private JFrame window;
	
	/** 执行队列 */
	private Set<Runnable> runnables = new HashSet<Runnable>();
	/** 临时队列，用于执行 */
	private Set<Runnable> executedRunnables = new HashSet<Runnable>();
	
	/* 单例 */
	private Application() { }
	static {
		try {
			// 解压natives
			String os = System.getProperty("os.name").toLowerCase();
			String arch = System.getProperty("os.arch").toLowerCase();
			boolean is64Bit = false;
			/*if (arch.equals("x86") || arch.equals("ppc") || arch.equals("PowerPC") || arch.equals("i386") || arch.equals("i686") || arch.equals("universal") || arch.equals("arm")) {
			    is64Bit = false;
			} else */if (arch.equals("amd64") || arch.equals("x86_64") || arch.equals("ppc64")) {
				is64Bit = true;
			}/* else {
			    throw new UnsupportedOperationException("Unsupported architecture: " + arch);
			}*/
			Platform platform = null;
			if (os.contains("windows")) {
				platform = is64Bit ? Platform.Windows64 : Platform.Windows32;
			} else if (os.contains("linux") || os.contains("freebsd") || os.contains("sunos")) {
				platform = is64Bit ? Platform.Linux64 : Platform.Linux32;
			} else if (os.contains("mac os x") || os.contains("darwin")) {
			    if (arch.startsWith("ppc")) {
			    	platform = is64Bit ? Platform.MacOSX_PPC64 : Platform.MacOSX_PPC32;
			    } else {
			    	platform = is64Bit ? Platform.MacOSX64 : Platform.MacOSX32;
			    }
			}/* else {
			    throw new UnsupportedOperationException("The specified platform: " + os + " is not supported.");
			}*/
			byte[] buf = new byte[1024];
			switch (platform) {
			case Windows64: {
				File lwjgl64 = new File(System.getProperty("user.dir"),
						"lwjgl64.dll");
				if (lwjgl64.exists())
					lwjgl64.delete();
				lwjgl64.createNewFile();
				InputStream in = Thread.currentThread().getContextClassLoader()
						.getResourceAsStream("lwjgl64.dll");
				OutputStream out = new FileOutputStream(lwjgl64);
				int len;
				while ((len = in.read(buf)) > 0) {
					out.write(buf, 0, len);
				}
				in.close();
				out.close();
				in = null;
				out = null;
			}
				{
					File openal64 = new File(System.getProperty("user.dir"),
							"OpenAL64.dll");
					if (openal64.exists())
						openal64.delete();
					openal64.createNewFile();

					InputStream in = Thread.currentThread()
							.getContextClassLoader()
							.getResourceAsStream("OpenAL64.dll");
					OutputStream out = new FileOutputStream(openal64);
					int len;
					while ((len = in.read(buf)) > 0) {
						out.write(buf, 0, len);
					}
					in.close();
					out.close();
					in = null;
					out = null;
				}
				break;
			case Windows32: {
				File lwjgl = new File(System.getProperty("user.dir"),
						"lwjgl.dll");
				if (lwjgl.exists())
					lwjgl.delete();
				lwjgl.createNewFile();
				InputStream in = Thread.currentThread().getContextClassLoader()
						.getResourceAsStream("lwjgl.dll");
				OutputStream out = new FileOutputStream(lwjgl);
				int len;
				while ((len = in.read(buf)) > 0) {
					out.write(buf, 0, len);
				}
				in.close();
				out.close();
				in = null;
				out = null;
			}
				{
					File openal32 = new File(System.getProperty("user.dir"),
							"OpenAL32.dll");
					if (openal32.exists())
						openal32.delete();
					openal32.createNewFile();

					InputStream in = Thread.currentThread()
							.getContextClassLoader()
							.getResourceAsStream("OpenAL32.dll");
					OutputStream out = new FileOutputStream(openal32);
					int len;
					while ((len = in.read(buf)) > 0) {
						out.write(buf, 0, len);
					}
					in.close();
					out.close();
					in = null;
					out = null;
				}
				break;
			case Linux64: {
				File lwjgl64 = new File(System.getProperty("user.dir"),
						"liblwjgl64.so");
				if (lwjgl64.exists())
					lwjgl64.delete();
				lwjgl64.createNewFile();
				InputStream in = Thread.currentThread().getContextClassLoader()
						.getResourceAsStream("liblwjgl64.so");
				OutputStream out = new FileOutputStream(lwjgl64);
				int len;
				while ((len = in.read(buf)) > 0) {
					out.write(buf, 0, len);
				}
				in.close();
				out.close();
				in = null;
				out = null;
			}
				{
					File openal64 = new File(System.getProperty("user.dir"),
							"libopenal64.so");
					if (openal64.exists())
						openal64.delete();
					openal64.createNewFile();

					InputStream in = Thread.currentThread()
							.getContextClassLoader()
							.getResourceAsStream("libopenal64.so");
					OutputStream out = new FileOutputStream(openal64);
					int len;
					while ((len = in.read(buf)) > 0) {
						out.write(buf, 0, len);
					}
					in.close();
					out.close();
					in = null;
					out = null;
				}
				break;
			case Linux32: {
				File lwjgl = new File(System.getProperty("user.dir"),
						"liblwjgl.so");
				if (lwjgl.exists())
					lwjgl.delete();
				lwjgl.createNewFile();
				InputStream in = Thread.currentThread().getContextClassLoader()
						.getResourceAsStream("liblwjgl.so");
				OutputStream out = new FileOutputStream(lwjgl);
				int len;
				while ((len = in.read(buf)) > 0) {
					out.write(buf, 0, len);
				}
				in.close();
				out.close();
				in = null;
				out = null;
			}
				{
					File openal = new File(System.getProperty("user.dir"),
							"libopenal.so");
					if (openal.exists())
						openal.delete();
					openal.createNewFile();

					InputStream in = Thread.currentThread()
							.getContextClassLoader()
							.getResourceAsStream("libopenal.so");
					OutputStream out = new FileOutputStream(openal);
					int len;
					while ((len = in.read(buf)) > 0) {
						out.write(buf, 0, len);
					}
					in.close();
					out.close();
					in = null;
					out = null;
				}
				break;
			case MacOSX_PPC32:
			case MacOSX32:
			case MacOSX_PPC64:
			case MacOSX64: {
				File lwjgl = new File(System.getProperty("user.dir"),
						"liblwjgl.dylib");
				if (lwjgl.exists())
					lwjgl.delete();
				lwjgl.createNewFile();
				InputStream in = Thread.currentThread().getContextClassLoader()
						.getResourceAsStream("liblwjgl.jnilib");
				OutputStream out = new FileOutputStream(lwjgl);
				int len;
				while ((len = in.read(buf)) > 0) {
					out.write(buf, 0, len);
				}
				in.close();
				out.close();
				in = null;
				out = null;
			}
				{
					File openal = new File(System.getProperty("user.dir"),
							"openal.dylib");
					if (openal.exists())
						openal.delete();
					openal.createNewFile();

					InputStream in = Thread.currentThread()
							.getContextClassLoader()
							.getResourceAsStream("openal.jnilib");
					OutputStream out = new FileOutputStream(openal);
					int len;
					while ((len = in.read(buf)) > 0) {
						out.write(buf, 0, len);
					}
					in.close();
					out.close();
					in = null;
					out = null;
				}
				break;
			}
			System.setProperty("org.lwjgl.librarypath", System.getProperty("user.dir"));
		} catch (IOException e) {
			JMir2Exception exception = JMir2Exception.valueOf(e, JMir2Exception.EXCEPTION_TYPE_CALC);
			log.error(exception.getMessage(), exception);
			throw exception;
		}
	}
	private static Application single = new Application();
	public static Application get() {
		return single;
	}
	
	
	/**
	 * 初始化
	 */
	public void initialize(ApplicationConfig config, ApplicationListener listener) {
		if(config.fullscreen) {
			// 如果希望全屏游戏，则只能以特定分辨率进行屏幕像素适配
			try {
				DisplayMode[] modes = Display.getAvailableDisplayModes();
				boolean available = false;
				for(DisplayMode mode : modes) {
					if(mode.getWidth() == config.width && mode.getHeight() == config.height) {
						available = true;
						Display.setDisplayMode(mode);
						Display.setFullscreen(true);
						// 标题
						Display.setTitle(config.title);
						// 图标
						Display.setIcon(new ByteBuffer[]{
								Kit.imageToIcon(config.icon16Path),
								Kit.imageToIcon(config.icon32Path),
								Kit.imageToIcon(config.icon128Path)
						});
						break;
					}
				}
				if(!available) {
					Sys.alert("不支持", "您的显卡不支持以下配置：全屏 " + config.width + "x" + config.height);
					System.exit(0);
				}
			} catch (LWJGLException e) {
				JMir2Exception exception = JMir2Exception.valueOf(e, JMir2Exception.EXCEPTION_TYPE_ENV);
				log.error(exception.getMessage(), exception);
				throw exception;
			}
		} else {
			if(config.parent != null && config.window != null) {
				try {
					Display.setParent(config.parent);
				} catch (LWJGLException e) {
					JMir2Exception exception = JMir2Exception.valueOf(e, JMir2Exception.EXCEPTION_TYPE_ENV);
					log.error(exception.getMessage(), exception);
					throw exception;
				}
				Application.this.window = config.window;
				config.window.addWindowListener(new WindowAdapter() {
					@Override
					public void windowClosing(WindowEvent e) {
						Application.this.running = !Application.this.listener.acceptCloseEvent();
					}
				});
			}
			try {
				Display.setDisplayMode(new DisplayMode(config.width, config.height));
			} catch (LWJGLException e) {
				JMir2Exception exception = JMir2Exception.valueOf(e, JMir2Exception.EXCEPTION_TYPE_ENV);
				log.error(exception.getMessage(), exception);
				throw exception;
			}
		}
		graphics.maxFps = config.fps;
		graphics.width = config.width;
		graphics.height = config.height;
		try {
			// 等待垂直同步
			Display.setVSyncEnabled(true);
			Display.create();
		} catch (LWJGLException e) {
			JMir2Exception exception = JMir2Exception.valueOf(e, JMir2Exception.EXCEPTION_TYPE_ENV);
			log.error(exception.getMessage(), exception);
			throw exception;
		}
		setupOpenGL();
		graphics.isNonpoweroftwoSupport = GLContext.getCapabilities().GL_ARB_texture_non_power_of_two;
		this.listener = listener;
	}
	
	/**
	 * 开始图形绘制主循环
	 * <br> FIXME 是否退出？包括AWT事件中的退出消息处理；游戏任务队列如何清理，绘制顺序等值得推敲
	 */
	public void mainLoop() {
		if(this.listener == null) {
			JMir2Exception exception = JMir2Exception.valueOf(null, JMir2Exception.EXCEPTION_TYPE_CALC);
			log.error(exception.getMessage(), exception);
			throw exception;
		}
		Event.initialize();
		graphics.lastTime = System.nanoTime();
		listener.create();
		while(this.running) {
			if(Display.isCloseRequested() && listener.acceptCloseEvent()) break;
			
			// 执行任务队列
			synchronized (runnables) {
				executedRunnables.clear();
				executedRunnables.addAll(runnables);
				runnables.clear();
			}
			Iterator<Runnable> itRunnable = executedRunnables.iterator();
			while(itRunnable.hasNext())
				itRunnable.next().run();
			
			// 是否退出
			this.running = !listener.isRequestExit();
			if(!this.running) break;
			
			// 加载资源
			cache.updateLoaders();
			// 更新时间
			graphics.updateTime();
			// 事件
			Event.poll();
			
			//清屏
			glClear(GL_COLOR_BUFFER_BIT);
			// 渲染
			listener.render();
			
			Display.update();
			Display.sync(graphics.maxFps);
		}
		
		// 循环结束
		listener.dispose();
		Display.destroy();
		System.exit(0);
	}

	
	/**
	 * 设置OpenGL参数
	 */
	private void setupOpenGL() {
		// 操作投影矩阵
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity(); // 复位
		// 将坐标系置为左上角(0,0)，右下角(width,height)
		glOrtho(0, graphics.width, graphics.height, 0, 1, -1);
		// 操作模型矩阵
		glMatrixMode(GL_MODELVIEW);
		// 开启2D纹理支持
		glEnable(GL_TEXTURE_2D);
		// 开启混合渲染
		glEnable(GL_BLEND);
		// 先使用普通混合
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	}
	
	/**
	 * 将执行任务加入队列
	 * <br>
	 * 本方法线程安全
	 * <br>
	 * 如果有需要在主线程处理的任务可以使用这个方法添加
	 * 
	 * @param runnable
	 * 		要在下次游戏主循环执行的任务<br>值得一提的是我们会先执行任务队列再退出(如果要退出的话)
	 */
	public void postRunnable(Runnable runnable) {
		synchronized (runnables) {
			runnables.add(runnable);
		}
	}
	
	/**
	 * 设置窗口标题
	 * <br>
	 * 由其他线程调用
	 * 
	 * @param title
	 * 		要设置的标题
	 */
	public void setTitle(final String title) {
		this.postRunnable(new Runnable() {
			public void run() {
				if(title == null) return;
				if(Application.this.window != null) Application.this.window.setTitle(title);
				else Display.setTitle(title);
			}
		});
	}
}